package math.vectorSpace.R3;

import java.awt.Graphics2D;
import math.mathExtended.MathExtended;
import math.vectorSpace.R3.visualization.Visualizable;
import math.vectorSpace.R3.visualization.Render3D;
import math.vectorSpace.R3.visualization.VisualizationProperties;
import math.vectorSpace.RX.Vector;

/**
 * Class for creating and working with vectors in R3.
 * Contains extensive methods for manipulating vectors.
 * 
 * @author Shimu Wu
 * @version 1.0
 */
public class Vector3D extends Vector<Vector3D> implements Visualizable {

    public static final Vector3D XAXIS = new Vector3D(1, 0, 0),
            YAXIS = new Vector3D(0, 1, 0),
            ZAXIS = new Vector3D(0, 0, 1);

    /**
     * Constructs and initialises the zero vector.
     */
    public Vector3D() {
        this(0, 0, 0);
    }

    /**
     * Constructs and initialises a vector that extends from (x1, y1, z1) 
     * to (x2, y2, z2).
     *
     * @param x1 the x-component of the first point
     * @param y1 the y-component of the first point
     * @param z1 the z-component of the first point
     * @param x2 the x-component of the second point
     * @param y2 the y-component of the second point
     * @param z2 the z-component of the second point
     */
    public Vector3D(double x1, double y1, double z1, double x2, double y2,
            double z2) {
        this(x2 - x1, y2 - y1, z2 - z1);
    }

    /**
     * Constructs and initialises a vector that extends from the origin 
     * to the given point.
     *
     * @param point a {@link Point3D} object
     */
    public Vector3D(Point3D point) {
        this(point.getX(), point.getY(), point.getZ());
    }

    /**
     * Constructs and initialises a vector that extends from point1 to point2.
     *
     * @param point1 a {@link Point3D} at the tail of the vector
     * @param point2 a {@link Point3D} at the head of the vector
     */
    public Vector3D(Point3D point1, Point3D point2) {
        this(point2.getX() - point1.getX(),
                point2.getY() - point1.getY(), point2.getZ() - point1.getZ());
    }

    private Vector3D(double[] components) {
        super(components);
    }

    /**
     * Constructs and initialises a vector from the given x, y, z components.
     *
     * @param x the x-component
     * @param y the y-component
     * @param z the z-component
     */
    public Vector3D(double x, double y, double z) {
        super(new double[]{x, y, z});
    }

    /**
     * Constructs and initialises a vector with the given length and direction.
     *
     * @param length the length of this vector
     * @param direction the Vector3D representing the direction of this vector
     */
    public Vector3D(double length, Vector3D direction) {
        super(direction);
        if (comparator.equal(length, 0)) {
            this.set(0, 0, 0);
        } else {
            double scaleFactor = length / (direction.length());
            this.set(direction.getX() * scaleFactor,
                    direction.getY() * scaleFactor,
                    direction.getZ() * scaleFactor);
        }
    }

    public Vector3D(Vector3D vector) {
        this(vector.getX(), vector.getY(), vector.getZ());
    }

    /**
     * Returns the x-component of this vector.
     *
     * @return the x-component of this vector
     */
    public final double getX() {
        return super.getComponent(0);
    }

    /**
     * Returns the y-component of this vector.
     *
     * @return the y-component of this vector
     */
    public final double getY() {
        return super.getComponent(1);
    }

    /**
     * Returns the z-component of this vector.
     *
     * @return the z-component of this vector
     */
    public final double getZ() {
        return super.getComponent(2);
    }

    /**
     * Sets the x-component of this vector.
     *
     * @param x the x-component
     */
    public final void setX(double x) {
        super.setComponent(0, x);
    }

    /**
     * Sets the y-component of this vector.
     *
     * @param y the y-component
     */
    public final void setY(double y) {
        super.setComponent(1, y);
    }

    /**
     * Sets the z-component of this vector.
     *
     * @param z the z-component
     */
    public final void setZ(double z) {
        super.setComponent(2, z);
    }

    /**
     * Sets the vector to the specified components.
     *
     * @param x the x-component
     * @param y the y-component
     * @param z the z-component
     */
    public final void set(double x, double y, double z) {
        setX(getX());
        setY(getY());
        setZ(getZ());
    }

    /**
     * Sets the this vector's components to the given point's coordinates.
     *
     * @param newPoint the {@link Point3D} to set this vector's components to
     */
    public void set(Point3D newPoint) {
        set(newPoint.getX(), newPoint.getY(), newPoint.getZ());
    }

    /**
     * Sets this vector's components to the given vector's components.
     *
     * @param newVector the Vector3D to set this vector to
     */
    public void set(Vector3D newVector) {
        set(newVector.getX(), newVector.getY(), newVector.getZ());
    }

    /**
     * Returns the angle in degrees between this vector and the x-axis.
     *
     * @return the angle between this vector and the x-axis in degrees
     */
    public double angleX() {
        return super.angle(0);
    }

    /**
     * Returns the angle in degrees between this vector and the y-axis.
     *
     * @return the angle between this vector and the y-axis in degrees
     */
    public double angleY() {
        return super.angle(1);
    }

    /**
     * Returns the angle in degrees between this vector and the z-axis.
     *
     * @return the angle between this vector and the z-axis in degrees
     */
    public double angleZ() {
        return super.angle(2);
    }

    /**
     * Rotate this vector around the x-axis
     *
     * @param angle
     * @return
     */
    public Vector3D rotateX(double angle) {
        angle = MathExtended.degreesToRad(angle);
        return new Vector3D(getX(), getY() * Math.cos(angle)
                - getZ() * Math.sin(angle), getY()
                * Math.sin(angle) + getZ() * Math.cos(angle));
    }

    /**
     * Rotate this vector around the y-axis
     *
     * @param angle
     * @return
     */
    public Vector3D rotateY(double angle) {
        angle = MathExtended.degreesToRad(angle);
        return new Vector3D(getX() * Math.cos(angle) - getZ()
                * Math.sin(angle), getY(), getX()
                * Math.sin(angle) + getZ() * Math.cos(angle));
    }

    /**
     * Rotate this vector counterclockwise around the z-axis
     *
     * @param angle
     * @return
     */
    public Vector3D rotateZ(double angle) {
        angle = MathExtended.degreesToRad(angle);
        return new Vector3D(getX() * Math.cos(angle) - getY()
                * Math.sin(angle), getX()
                * Math.sin(angle) + getY() * Math.cos(angle), getZ());
    }

    public Vector3D rotate(Vector3D axis, double angle) {

        if (this.isColinear(axis)) {
            return new Vector3D(this);
        }

        // This angle will rotate on a plane where the normal
        // of the plane is the given axis

        // We can set up a 2D coordinate system on the plane
        // the "xAxis" of the plane will be this vector
        // the "yAxis" of the plane can be obtained by crossing
        // the normal with the xAxis
        Vector3D xAxis, yAxis;
        xAxis = this;
        yAxis = this.cross(axis);

        // This vector with respect to the new 2D coordinate system
        // will have an x-value of the length of the vector (since 
        // itself is the x-axis) and a y-value of 0
        double x, y, length;
        length = this.length();

        // This becomes a really simple rotation in 2D
        x = MathExtended.cosd(angle) * length;
        y = MathExtended.sind(angle) * length;

        // Convert it back into 3D
        Vector3D xComponent, yComponent;
        xComponent = xAxis.scaleToSize(x);
        yComponent = yAxis.scaleToSize(y);

        return xComponent.add(yComponent);
    }

    /**
     * Rotate this vector towards the given vector by the given angle in degrees.
     *
     * @param v2 a Vector3D to rotate this vector towards
     * @param angle the angle of rotation in degrees
     * @return a new Vector3D that is this vector rotated towards v2 by the given angle.
     */
    public Vector3D rotateTowards(Vector3D v2, double angle) {

        if (this.isColinear(v2)) {
            return new Vector3D(this);
        }

        Vector3D normal, xAxis, yAxis;

        // This vector and the given vector lie on the same plane
        // that has the normal calculated below
        normal = this.cross(v2);
        
        // Use v2 as the "y-axis" in the plane
        yAxis = v2;

        // Cross the "yAxis" with the normal to get the "x-axis" in the plane
        xAxis = yAxis.cross(normal);

        /*
        System.out.println("this " + this);
        System.out.println("axis " + axis);
        System.out.println("Normal " + normal);
        System.out.println("yAxis " + yAxis);
        System.out.println("xAxis " + xAxis);
         */

        double x, y, length;

        // Get the coordinates of this vector with respect to the xAxis 
        // and yAxis in the plane
        x = this.projectMag(xAxis);
        y = this.projectMag(yAxis);
        length = Math.sqrt(Math.pow(getX(), 2) + Math.pow(getY(), 2));

        angle += MathExtended.aTand(y / x);

        // Get the coordinates of the rotated vector
        x = MathExtended.cosd(angle) * length;
        y = MathExtended.sind(angle) * length;

        Vector3D xComponent, yComponent;
        xComponent = xAxis.scaleToSize(x);
        yComponent = yAxis.scaleToSize(y);

        return xComponent.add(yComponent);
    }

    /**
     * Scales this vector to the given length.
     *
     * @param length
     * @return
     */
    public Vector3D scaleToSize(double length) {
        return new Vector3D(this.unitVector().scale(length));
    }

    public Vector3D add(double x2, double y2, double z2) {
        return new Vector3D(getX() + x2, getY() + y2, getZ() + z2);
    }

    public Vector3D add(Point3D p) {
        return new Vector3D(getX() + p.getX(), getY() + p.getY(), getZ() + p.getZ());
    }

    /**
     * Returns the cross product of this vector and the given vector.
     *
     * @param v2 the other Vector3D
     * @return the cross product of the two vectors
     */
    public Vector3D cross(Vector3D v2) {
        double cX = (getY() * v2.getZ() - getZ() * v2.getY());
        double cY = (getZ() * v2.getX() - getX() * v2.getZ());
        double cZ = (getX() * v2.getY() - getY() * v2.getX());

        return new Vector3D(cX, cY, cZ);
    }

    public static Vector3D[] generateVectors(Double[] components) {
        Vector3D[] vectors = new Vector3D[components.length / 3];
        int count = 0;
        for (int i = 0; i < components.length; i += 3) {
            vectors[count] = new Vector3D(components[i], components[i + 1],
                    components[i + 2]);
            count++;
        }

        return vectors;
    }

    public static Vector3D[] generateVectors(Integer[] components) {
        Vector3D[] vectors = new Vector3D[components.length / 3];
        int count = 0;
        for (int i = 0; i < components.length; i += 3) {
            vectors[count] = new Vector3D(components[i], components[i + 1],
                    components[i + 2]);
            count++;
        }

        return vectors;
    }

    /**
     * Returns the String representation of this vector of the form: 
     * Vector3D [getX(), getY(), getZ()].
     *
     * @return the String representation of this vector
     */
    @Override
    public String toString() {
        return ("Vector3D [" + getX() + ", " + getY() + ", " + getZ() + "]");
    }

    public static void main(String args[]) {
        // Do some quick tests here

        Vector3D v = new Vector3D(1, Math.sqrt(3), 0);
        Vector3D v2 = new Vector3D(1, 0, 0);

        System.out.println(v.angleD(v2));
        System.out.println(v2.angleD(v));
    }

    @Override
    public boolean equals(Object o) {
        if (this.getClass().equals(o.getClass())) {
            return this.isEqual((Vector3D) o);
        }
        return false;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 59 * hash
                + (int) (Double.doubleToLongBits(this.getX())
                ^ (Double.doubleToLongBits(this.getX()) >>> 32));
        hash = 59 * hash
                + (int) (Double.doubleToLongBits(this.getY())
                ^ (Double.doubleToLongBits(this.getY()) >>> 32));
        hash = 59 * hash
                + (int) (Double.doubleToLongBits(this.getZ())
                ^ (Double.doubleToLongBits(this.getZ()) >>> 32));
        return hash;
    }

    @Override
    public void draw(Render3D render, Graphics2D graphics,
            VisualizationProperties properties) {
        Segment3D segment = new Segment3D(0, 0, 0, this.getX(),
                this.getY(), this.getZ());
        segment.draw(render, graphics, properties);
        //Vector2D vector2D = new Vector2D(l.getStart(), l.getEnd());

        //double arrowLength = 20;
        //double arrowAngle = 30;
        //drawArrow(vector2D);
    }

    public Point3D toPoint() {
        return new Point3D(this);
    }
    /*
    private void drawArrow(Vector2D head) {
    Polygon arrowHead = new Polygon();
    Vector2D arrowHeight = head.unitVector().scale(arrowLength);
    Vector2D tail = head.subtract(arrowHeight);
    Vector2D normal = new Vector2D(arrowHeight.getNormal(),
    arrowHeight.length() * Operator.tand(arrowAngle) / 2.0);
    
    arrowHead.addPoint((int) r.translateX(head.getX()),
    (int) r.translateY(head.getY()));
    arrowHead.addPoint((int) r.translateX(normal.add(tail).getX()),
    (int) r.translateY(normal.add(tail).getY()));
    arrowHead.addPoint((int) r.translateX(tail.add(
    arrowHeight.scale(0.2)).getX()), (int) r.translateY(tail
    .add(arrowHeight.scale(0.2)).getY()));
    arrowHead.addPoint(
    (int) r.translateX(normal.invert().add(tail).getX()),
    (int) r.translateY(normal.invert().add(tail).getY()));
    g.fill(arrowHead);
    
    }
     */

    @Override
    protected Vector3D createVector(double[] components) {
        return new Vector3D(components);
    }
}
